<?php

require_once 'db.inc';
require_once 'data.inc';
require_once 'session.inc';


class Group
{
	const rootId = null;
	
	function __construct($id)
	{
		$this->id = self::toID($id);
		
		static $q = null;
		createStaticQuery($q,"
			SELECT parent_id, name, enabled, code, page_title, page_keywords, page_descr
			FROM cf_groups
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No Group with id [{$this->id}]");
		}
		
		$this->parentId = $q->at(0);
		$this->name = $q->at(1);
		$this->enabled = (bool)$q->at(2) == 1;
		$this->code = trim($q->at(3));
		$this->pageTitle = $q->at(4);
		$this->pageKeywords = $q->at(5);
		$this->pageDescr = $q->at(6);
		
		$q->close();
	}
	
	public function id() 
	{
		return $this->id;
	}
	
	/** @return  string CODE or numeric ID */
	public function code() 
	{
		return strlen($this->code) ? $this->code : $this->id;
	}
	
	/**
		@param $id  string CODE or numeric ID
		@return numeric ID
	*/
	static public function toID($id)
	{
		if (is_numeric($id)) {
			return (int)$id;
		}
		
		if (is_null($id)) {		//root group
			return $id;
		}

		static $q = null;
		createStaticQuery($q,"
			SELECT id FROM cf_groups WHERE code=:code
		");
		$q->setParam('code',trim($id));
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("Invalid group code: [$id]");
		}
		$id = (int)$q->at(0);
		$q->close();
		
		return $id;
	}

	public function parentId() 
	{
		return $this->parentId;
	}

	public function name() 
	{
		return $this->name;
	}
	
	public function enabled() 
	{
		return $this->enabled;
	}
	
	public function descr() 
	{
		if (!is_null($this->descr)) {
			return $this->descr;
		}
		
		static $q = null;
		createStaticQuery($q,"
			SELECT descr
			FROM cf_groups
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No Group with id [{$this->id}]");
		}
		$this->descr = $q->at(0);
		$q->close();
	
		return $this->descr;
	}
	
	public function pageTitle() 
	{
		return $this->pageTitle;
	}

	public function pageKeywords() 
	{
		return $this->pageKeywords;
	}

	public function pageDescr() 
	{
		return $this->pageDescr;
	}
	
	public function nestingDepth()
	{
		if (!is_null($this->nestingDepth)) {
			return $this->nestingDepth;
		}
		
		$this->nestingDepth = 0;
		
		$id = $this->parentId();
		while ($id != Group::rootId)
		{
			++$this->nestingDepth;
			$gr = new Group($id);
			$id = $gr->parentId();
		}
		
		return $this->nestingDepth;
	}
	
	public function hasProducts()
	{
		if (!is_null($this->hasProducts)) {
			return $this->hasProducts;
		}
		
		static $q = null;
		createStaticQuery($q,"
			SELECT product_id
			FROM cf_products_in_groups
			WHERE group_id=:id
			LIMIT 1
		");
		
		$q->setParam('id',$this->id);
		$q->execute();
		$this->hasProducts = (bool)$q->fetch();
		$q->close();
	
		return $this->hasProducts;
	}
	
	public function __toString() 
	{
		return "[{$this->id()}] {$this->name()}";
	}
	

	private $id = null;
	private $code = null;
	private $parentId = null;
	private $name = '';
	private $descr = null;
	private $enabled = null;
	private $nestingDepth = null;
	private $hasProducts = null;
	private $pageTitle = null;
	private $pageKeywords = null;
	private $pageDescr = null;
}


class Groups
	extends ArrayOfItems
{
	public function names()
	{
		$names = array();
		foreach ($this->items as $group)
		{
			$names[] = $group->name();
		}
		return $names;
	}
	
	public function IDs()
	{
		$ids = array();
		foreach ($this->items as $group)
		{
			$ids[] = $group->id();
		}
		return $ids;
	}
	
	public function get($id) 
	{
		$id = Group::toID($id);
		foreach ($this->items as $group) 
		{
			if ($group->id() == $id) {
				return $group;
			}
		}
		return false;
	}
	
	public function exist($id) 
	{
		return $this->get($id) !== false;
	}
	
	
	protected function add(Group $item) 
	{
		parent::add($item);
	}
};


class ChildGroups
	extends Groups
{
	function __construct($parentId, $showDisabled=false)
	{
		$parentId = Group::toID($parentId);
		$q = createQuery("
			SELECT id
			FROM cf_groups
			WHERE (parent_id=:parentId OR (:parentId IS NULL AND parent_id IS NULL))
			". ($showDisabled ? '' : 'AND enabled=1') . "
			ORDER BY sort_order ASC
		");
		$q->setParam('parentId',$parentId);
		$q->execute();
		while ($q->fetch()) {
			$this->add(new Group($q->at(0)));
		}
		$q->close();
	}	
};


class DescendantGroups
	extends Groups
{
	function __construct($ancestorId)
	{
		$this->addChildren($ancestorId);
	}
	
	///array( parentId => array(child1, child2, ...) )
	public function toParentsArray()
	{
		$parents = array();
		foreach ($this->items as $child) 
		{
			$parentId = $child->parentId();
			if (!array_key_exists($parentId, $parents)) {
				$parents[$parentId] = array();
			}
			$parents[$parentId][] = $child;
		}
		return $parents;
	}
	
	
	private function addChildren($parentId)
	{
		$children = new ChildGroups($parentId);
		foreach ($children as $child) 
		{
			$this->add($child);
			$this->addChildren($child->id());
		}
	}
};


class AncestorGroups   /** [0]grandparent [1]parent */
	extends Groups
{
	function __construct($id)
	{
		$id = Group::toID($id);
		while ($id != Group::rootId)
		{                                            
			$gr = new Group($id);
			$id = $gr->parentId();
			if ($id != Group::rootId) {
				$gr = new Group($id);
				$this->add($gr);
			}                                         
		}
		$this->items = array_reverse($this->items);
	}
};


class Manufacturer
{
	function __construct($id)
	{
		$this->id = self::toID($id);
		
		static $q = null;
		createStaticQuery($q,"
			SELECT name, code
			FROM cf_manufacturers
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No Manufacturer with id [{$this->id}]");
		}
		
		$this->name = $q->at(0);
		$this->code = trim($q->at(1));
		
		$q->close();
	}
	
	public function id() 
	{
		return $this->id;
	}
	
	public function code() 
	{
		return strlen($this->code) ? $this->code : $this->id;
	}
	
	/**
		@param $id  string CODE or numeric ID
		@return numeric ID
	*/
	static public function toID($id)
	{
		if (is_numeric($id)) {
			return (int)$id;
		}

		static $q = null;
		createStaticQuery($q,"
			SELECT id FROM cf_manufacturers WHERE code=:code
		");
		$q->setParam('code',trim($id));
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("Invalid manufacturer code: [$id]");
		}
		$id = (int)$q->at(0);
		$q->close();
		
		return $id;
	}

	public function name() 
	{
		return $this->name;
	}
	
	public function descr() 
	{
		if (!is_null($this->descr)) {
			return $this->descr;
		}
		
		static $q = null;
		createStaticQuery($q,"
			SELECT descr
			FROM cf_manufacturers
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No Manufacturer with id [{$this->id}]");
		}
		$this->descr = $q->at(0);
		$q->close();
		
		return $this->descr;
	}
	
	//image file URL (false if none)
	public function logo()
	{
		if (!is_null($this->logo)) {
			return $this->logo;
		}
		
		$logo = new Files('manufacturers');
		$logo->open($this->id, 'logo');
		if ($logo->fetch()) {
			$this->logo = $logo->current();
		}
		else {
			$this->logo = false;
		}

		return $this->logo;
	}
	
	
	private $id = null;
	private $code = null;
	private $name = '';
	private $descr = null;
	private $logo = null;
};



class ManufacturersInGroup
	extends ArrayOfItems
{
	function __construct($groupId)
	{
		$groupId = Group::toID($groupId);
		$descendants = new DescendantGroups($groupId);
		$IDs = $descendants->IDs();
		$IDs[] = $groupId;
		$q = createQuery("
			SELECT DISTINCT cf_products.manufacturer_id
			FROM cf_products
			INNER JOIN cf_products_in_groups ON cf_products.id=cf_products_in_groups.product_id
			WHERE cf_products_in_groups.group_id IN (".implode(',',$IDs).")
		");
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add( new Manufacturer($q->at(0)) );
		}
		$q->close();
	}
};



class ProductGroups
	extends Groups
{
	function __construct($productId, $showDisabled=false)
	{
		$productId = Product::toID($productId);
		$q = createQuery("
			SELECT group_id
			FROM cf_products_in_groups
			INNER JOIN cf_groups ON cf_products_in_groups.group_id = cf_groups.id
			WHERE product_id=:productId
			". ($showDisabled ? '' : 'AND cf_groups.enabled=1') . "
			ORDER BY cf_groups.sort_order ASC
		");
		$q->setParam('productId',$productId);
		$q->execute();
		while ($q->fetch()) 
		{
			$id = $q->at(0);
			if ($showDisabled || $this->isGroupEnable($id))
			{
				$this->add(new Group($id));
			}
		}
		$q->close();
	}
	
	
	private function isGroupEnable($id) 
	{
		$ancestors = new AncestorGroups($id);
		foreach ($ancestors as $ancestor)
		{
			if (!$ancestor->enabled())
			{
				return false;
			}
		}
		return true;
	}
};


class Product
{
	function __construct($id)
	{
		$this->id = self::toID($id);
		
		static $q = null;
		createStaticQuery($q,"
			SELECT 
				cf_products.manufacturer_id, 
				cf_manufacturers.name AS manufacturer_name,
				cf_products.name, 
				cf_products.model, 
				cf_products.price, 
				cf_product_states.id as state_id,
				cf_product_states.name as state,
				cf_products.code,
				cf_products.tags,
				cf_products.discount
			FROM cf_products
			INNER JOIN cf_product_states ON cf_products.state_id=cf_product_states.id
			INNER JOIN cf_manufacturers ON cf_products.manufacturer_id=cf_manufacturers.id
			WHERE cf_products.id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No product with id [{$this->id}]");
		}
		$this->manufacturerId = $q->at(0);
		$this->manufacturerName = $q->at(1);
		$this->name = $q->at(2);
		$this->model = $q->at(3);
		$this->price = $q->at(4);
		$this->stateId = $q->at(5);
		$this->state = $q->at(6);
		$this->code = trim($q->at(7));
		$this->tags = explode(',',trim($q->at(8)));
		$this->discount = trim($q->at(9));
		$q->close();
	}

	public function id() 
	{
		return $this->id;
	}
	
	public function code() 
	{
		return strlen($this->code) ? $this->code : $this->id;
	}
	
	/**
		@param $id  string CODE or numeric ID
		@return numeric ID
	*/
	static public function toID($id)
	{
		if (is_numeric($id)) {
			return (int)$id;
		}

		static $q = null;
		createStaticQuery($q,"
			SELECT id FROM cf_products WHERE code=:code
		");
		$q->setParam('code',trim($id));
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("Invalid product code: [$id]");
		}
		$id = (int)$q->at(0);
		$q->close();
		
		return $id;
	}

	public function name() 
	{
		return $this->name;
	}
	
	public function model() 
	{
		return $this->model;
	}
	
	public function manufacturerId() 
	{
		return $this->manufacturerId;
	}
	
	public function manufacturerName() 
	{
		return $this->manufacturerName;
	}

	public function hasPrice()
	{
		return $this->price != 0;
	}
	
	public function price()
	{
		if ($this->discount())
		{
			return max($this->price - $this->discount(),0);
		}
		return $this->price;
	}
	
	public function priceNoDiscount()
	{
		return $this->price;
	}
	
	/** @return  product price (with discount) if it is set or min of options prices */
	public function minPrice()
	{
		if ($this->hasPrice())
		{
			return $this->price();
		}
		
		if (is_null($this->minPriceOption))
		{
			$this->minPriceOption = $this->getMinPriceOption();
		}
		if ($this->minPriceOption)
		{
			return $this->minPriceOption->price();
		}
		return 0;
	}

	public function hasDiscount()
	{
		return $this->hasPrice() && strlen($this->discount) != 0;
	}	
	/**
		@return  discount as integer
	*/
	public function discount()
	{
		return (int)$this->discount;
	}

	
	public function minPriceOptionId()
	{
		if ($this->hasPrice())
		{
			return 0;
		}
		
		if (is_null($this->minPriceOption))
		{
			$this->minPriceOption = $this->getMinPriceOption();
		}
		if ($this->minPriceOption)
		{
			return $this->minPriceOption->id();
		}
		return 0;
	}
	
	public function shortDescr() 
	{
		if (!is_null($this->shortDescr)) {
			return $this->shortDescr;
		}
		
		static $q = null;
		createStaticQuery($q,"
			SELECT short_descr
			FROM cf_products
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No product with id [{$this->id}]");
		}
		$this->shortDescr = $q->at(0);
		$q->close();
		
		return $this->shortDescr;
	}
	
	public function fullDescr() 
	{
		if (!is_null($this->fullDescr)) {
			return $this->fullDescr;
		}
		
		static $q = null;
		createStaticQuery($q,"
			SELECT full_descr
			FROM cf_products
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No product with id [{$this->id}]");
		}
		$this->fullDescr = $q->at(0);
		$q->close();
		
		return $this->fullDescr;
	}

	public function stateId()
	{
		return $this->stateId;
	}

   public function state()
   {
      return $this->state;
   }
   
   /** @return  array of tag words */
   public function tags()
   {
      return $this->tags;
   }
	
	public function clearAttributes()
	{
		self::delAttributes($this->id());
	}
	
	public function setAttributes(Attributes $attrs)
	{
		$this->clearAttributes();
		
		$availAttrs = new ProductAttributes($this->id(), true);
		foreach ($attrs as $attr)
		{
			if (!$availAttrs->exist($attr->id())) 
			{
				throw new Exception("
					Attribute [{$attr->id()}] {$attr->name()}
					is not of product [{$this->id()}] {$this->name()}
				");
			}
			$this->addAttrValue($attr);
		}
	}
	
	///array of image urls
	public function images()
	{
		if (!is_null($this->images)) {
			return $this->images;
		}

		$images = new Files('products');
		$images->open(
			$this->id(), 
			array(
				'image1','image2','image3','image4','image5',
				'image6','image7','image8','image9','image10'
			)
		);
		$this->images = array();
		while ($images->fetch()) 
		{
			$this->images[] = $images->current();
		}
		
		return $this->images;
	}
	
	public function __toString()
	{
		return $this->name().' '.$this->manufacturerName().($this->model() ? (' '.$this->model()) : '');
	}
		
	
	static public function del($productId)
	{
		$productId = self::toID($productId);
		
		self::delFromGroups($productId);
		self::delAttributes($productId);
		self::delOptions($productId);
		
		static $q = null;
		createStaticQuery($q,"
			DELETE FROM cf_products
			WHERE id=:productId
		");
		$q->setParam('productId',$productId);
		$q->execute();
		$q->close();
	}
	
	
	
	static private function delFromGroups($productId)
	{
		static $q = null;
		createStaticQuery($q,"
			DELETE FROM cf_products_in_groups
			WHERE product_id=:productId
		");
		$q->setParam('productId',$productId);
		$q->execute();
		$q->close();
	}
	
	static private function delAttributes($productId)
	{
		static $q = null;
		createStaticQuery($q,"
			DELETE FROM cf_product_attributes
			WHERE product_id=:productId
		");
		$q->setParam('productId',$productId);
		$q->execute();
		$q->close();
	}

	static private function delOptions($productId)
	{
		$options = new ProductOptions($productId);
		foreach ($options as $option) {
			ProductOption::del($option->id());
		}
	}
	
	private function addAttrValue(Attribute $attr) 
	{
		static $q = null;
		createStaticQuery($q,"
			INSERT INTO cf_product_attributes
				(product_id, attribute_id, attribute_value)
			VALUES (:productId, :attrId, :value)
		");
		$q->setParam('productId', $this->id());
		$q->setParam('attrId', $attr->id());
		$q->setParam('value', $attr->value());
		$q->execute();
		$q->close();
	}	
	
	private function getMinPriceOption()
	{
		static $q = null;
		createStaticQuery($q,"
			SELECT id, price
			FROM cf_product_options
			WHERE product_id=:productId
			  AND price > 0
			LIMIT 1
		");
		$q->setParam('productId',$this->id());
		$q->execute();
		if ($q->fetch())
		{
			return new ProductOption($q->at(0));
		}
		
		return false;
	}
	
	private $id = null;
	private $code = null;
	private $name = '';
	private $model = '';
	private $manufacturerId = null;
	private $manufacturerName = null;
	private $shortDescr = null;
	private $fullDescr = null;
	private $images = null;
	private $price = null;
	private $minPriceOption = null;
	private $stateId;
	private $state;
	private $tags = array();
	private $discount = null;
};



class ProductOption
{
	function __construct($id)
	{
		$this->id = self::toID($id);
		
		static $q = null;
		createStaticQuery($q,"
			SELECT product_id, name, descr, price, code
			FROM cf_product_options
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No product option with id [{$this->id}]");
		}
		
		$this->productId = $q->at(0);
		$this->name = $q->at(1);
		$this->descr = $q->at(2);
		$this->price = $q->at(3);
		$this->code = trim($q->at(4));
		
		$q->close();
	}

	public function id() 
	{
		return $this->id;
	}
	
	public function code() 
	{
		return strlen($this->code) ? $this->code : $this->id;
	}
	
	/**
		@param $id  string CODE or numeric ID
		@return numeric ID
	*/
	static public function toID($id)
	{
		if (is_numeric($id)) {
			return (int)$id;
		}

		static $q = null;
		createStaticQuery($q,"
			SELECT id FROM cf_product_options WHERE code=:code
		");
		$q->setParam('code',trim($id));
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("Invalid product option code: [$id]");
		}
		$id = (int)$q->at(0);
		$q->close();
		
		return $id;
	}
	
	public function productId() 
	{
		return $this->productId;
	}

	public function name() 
	{
		return $this->name;
	}
	
	public function descr() 
	{
		return $this->descr;
	}
	
	public function hasPrice()
	{
		return $this->price != 0;
	}
	
	public function price()
	{
		return $this->price;
	}
	
	public function clearAttributes()
	{
		self::delAttributes($this->id());
	}
	
	public function setAttributes(Attributes $attrs)
	{
		$this->clearAttributes();
		
		$availAttrs = new ProductAttributes($this->productId(), true);
		foreach ($attrs as $attr)
		{
			if (!$availAttrs->exist($attr->id())) 
			{
				throw new Exception("
					Attribute [{$attr->id()}] {$attr->name()}
					is not of product [{$this->id()}] {$this->name()}
				");
			}
			if (!$attr->isNull()) 
			{
				$this->addAttrValue($attr);
			}
		}
	}
	
	static public function del($optionId)
	{
		self::delAttributes($optionId);
		
		static $q = null;
		createStaticQuery($q,"
			DELETE FROM cf_product_options
			WHERE id=:optionId
		");
		$q->setParam('optionId',$optionId);
		$q->execute();
		$q->close();
	}
	
	public function __toString() 
	{
		return "[{$this->id()}] {$this->name()} (price: {$this->price()})";
	}
	
	
	
	static private function delAttributes($optionId)
	{
		static $q = null;
		createStaticQuery($q,"
			DELETE FROM cf_product_options_attributes
			WHERE product_option_id=:optionId
		");
		$q->setParam('optionId',$optionId);
		$q->execute();
		$q->close();
	}
	
	private function addAttrValue(Attribute $attr) 
	{
		static $q = null;
		createStaticQuery($q,"
			INSERT INTO cf_product_options_attributes
				(product_option_id, attribute_id, attribute_value)
			VALUES 
				(:optionId, :attrId, :value)
		");
		$q->setParam('optionId', $this->id());
		$q->setParam('attrId', $attr->id());
		$q->setParam('value', $attr->value());
		$q->execute();
		$q->close();
	}
	
	private $id = null;	
	private $code = null;
	private $productId = null;
	private $name = '';
	private $descr = '';
	private $price = null;
};



class ProductOptions
	extends ArrayOfItems
{
	function __construct($productId)
	{
		$productId = Product::toID($productId);
		static $q = null;
		createStaticQuery($q,"
			SELECT id
			FROM cf_product_options
			WHERE product_id=:productId
			ORDER BY sort_order
		");
		$q->setParam('productId',$productId);
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add(new ProductOption($q->at(0)));
		}
		$q->close();
	}
};



class LimitedArray
	extends ArrayOfItems
{
	protected function __construct($sql, $skip=null, $limit=null)
	{
		$this->sql = $sql;
		$this->limitedSql = ($skip || $limit) ? limitSelect($sql, $limit, $skip) : $sql;
	}

	public function countAll()
	{
		return DB::countRecords($this->sql);
	}
	
	protected function limitedSQL()
	{
		return $this->limitedSql;
	}
	
	private $sql = null;
	private $limitedSql = null;
}



class Products
	extends LimitedArray
{
	public function IDs()
	{
		$ids = array();
		foreach ($this->items as $product)
		{
			$ids[] = $product->id();
		}
		return $ids;
	}
};



class ProductsInGroup
	extends Products
{
	function __construct($groupId, $skip=null, $limit=null)
	{
		$sql = "
			SELECT product_id
			FROM cf_products_in_groups
			INNER JOIN cf_products ON cf_products.id = cf_products_in_groups.product_id
			WHERE group_id=:groupId
			  AND (cf_products.enabled_from IS NULL OR NOW() >= cf_products.enabled_from)
			  AND (cf_products.enabled_to IS NULL OR NOW() < cf_products.enabled_to)
			ORDER BY sort_order,cf_products.name,cf_products.id
		";
		parent::__construct($sql,$skip,$limit);
		
		$groupId = Group::toID($groupId);
		$q = createQuery($this->limitedSql());
		$q->setParam('groupId',$groupId);
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add(new Product($q->at(0)));
		}
		$q->close();
	}	
};



/**
	Products that belong to ANY group in the first set and to EVERY group in the second set
*/
class ProductsInGroups
	extends Products
{
	/**
		@param  $groupIDs          array of IDs or CODEs
		@param  $mustBeInGroupIDs  array of IDs or CODEs
		@param  $sort              array('sort_field1','sort_field2'=>'ASC|DESC',...)  ASC by default  sort_field=sort_order|name|model|id
	*/
	function __construct($groupIDs, $skip=null, $limit=null, $mustBeInGroupIDs=array(), $sort=array('sort_order','name','id'))
	{
		foreach ($groupIDs as $i => $id)
		{
			$groupIDs[$i] = Group::toID($id);
		}
		
		$existsSQL = '';
		foreach ($mustBeInGroupIDs as $i => $id)
		{
			$existsSQL .= " AND EXISTS(SELECT * FROM cf_products_in_groups WHERE product_id=cf_products.id AND group_id=".Group::toID($id).")";
		}
		
		$sortSQL = '';
		foreach ($sort as $f => $d)
		{
			if ($sortSQL) {
				$sortSQL .= ',';
			}
			if (is_string($f)) {
				$sortSQL .= "$f $d";
			}
			else {
				$sortSQL .= "$d ASC";
			}
		}
		
		$sql = "
			SELECT DISTINCT cf_products.id
			FROM cf_products
			INNER JOIN cf_products_in_groups ON cf_products.id = cf_products_in_groups.product_id
			WHERE group_id IN (".implode(',',$groupIDs).")
			  $existsSQL
			  AND (cf_products.enabled_from IS NULL OR NOW() >= cf_products.enabled_from)
			  AND (cf_products.enabled_to IS NULL OR NOW() < cf_products.enabled_to)
			". (empty($sort) ? '' : "ORDER BY $sortSQL")
		;
		
		parent::__construct($sql,$skip,$limit);
		
		$q = createQuery($this->limitedSql());
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add(new Product($q->at(0)));
		}
		$q->close();
	}
};





class RelatedProducts
      extends Products
{
   function __construct($productId)
   {
		$productId = Product::toID($productId);
  		static $q = null;
		createStaticQuery($q,"
			SELECT related_product_id
			FROM cf_related_products
			INNER JOIN cf_products ON cf_products.id = cf_related_products.related_product_id
			WHERE product_id=:productId
	          AND (cf_products.enabled_from IS NULL OR NOW() >= cf_products.enabled_from)
			  AND (cf_products.enabled_to IS NULL OR NOW() < cf_products.enabled_to)
			ORDER BY sort_order,cf_products.name
		");
		$q->setParam('productId',$productId);
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add(new Product($q->at(0)));
		}
		$q->close();
   }
};



class FindProducts
	extends Products
{
	function __construct($search, $skip=null, $limit=null)
	{
		$this->search = $search;
		$sql = "
			SELECT id
			FROM cf_products
			WHERE (name LIKE CONCAT('%',:search,'%')
			OR model LIKE CONCAT('%',:search,'%')
			OR short_descr LIKE CONCAT('%',:search,'%')
			OR full_descr LIKE CONCAT('%',:search,'%'))
			AND (enabled_from IS NULL OR NOW() >= enabled_from)
			AND (enabled_to IS NULL OR NOW() < enabled_to)
			ORDER BY name
		";
		parent::__construct($sql,$skip,$limit);
		
		$q = createQuery($this->limitedSql());
		$q->setParam('search',$this->search);
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add(new Product($q->at(0)));
		}
		$q->close();
	}
	
	public function countAll()
	{
		if (is_null($this->count))
		{
			$q = createQuery("
					SELECT COUNT(*)
					FROM cf_products
					WHERE (name LIKE CONCAT('%',:search,'%')
					OR model LIKE CONCAT('%',:search,'%')
					OR short_descr LIKE CONCAT('%',:search,'%')
					OR full_descr LIKE CONCAT('%',:search,'%'))
					AND (enabled_from IS NULL OR NOW() >= enabled_from)
					AND (enabled_to IS NULL OR NOW() < enabled_to)
				");
			$q->setParam('search',$this->search);
			$q->execute();
			$q->fetch();
			$this->count = $q->at(0);
		}
		return $this->count;
	}
	
	private $search = null;
	private $count = null;
};



class FindProductsByTag
	extends Products
{
	function __construct($tag, $skip=null, $limit=null)
	{
		$this->tag = $tag;
		$sql = "
			SELECT id
			FROM cf_products
			WHERE FIND_IN_SET(:tag,tags)
			  AND (enabled_from IS NULL OR NOW() >= enabled_from)
			  AND (enabled_to IS NULL OR NOW() < enabled_to)
			ORDER BY name
		";
		parent::__construct($sql,$skip,$limit);
		
		$q = createQuery($this->limitedSql());
		$q->setParam('tag',$tag);
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add(new Product($q->at(0)));
		}
		$q->close();
	}
	
	public function countAll()
	{
		if (is_null($this->count))
		{
			$q = createQuery("
				SELECT COUNT(*)
				FROM cf_products
				WHERE FIND_IN_SET(:tag,tags)
					AND (enabled_from IS NULL OR NOW() >= enabled_from)
					AND (enabled_to IS NULL OR NOW() < enabled_to)
				",
				$this->tag
			);
			$q->fetch();
			$this->count = $q->at(0);
		}
		return $this->count;
	}
	
	private $tag = null;
	private $count = null;
};




class AttributeTypes
{
	const STRING = 1;
	const INTEGER = 3;
	const PRICE = 6;
	const FLOAT1 = 77;
	const FLOAT2 = 88;
	const FLOAT3 = 99;
};


class Attribute
{
	function __construct($id)
	{
		$this->id = self::toID($id);
		
		static $q = null;
		createStaticQuery($q,"
			SELECT name, type_id, code
			FROM cf_attributes
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No Attribute with id [{$this->id}]");
		}
		$this->name = $q->at(0);
		$this->type = $q->at(1);
		$this->code = trim($q->at(2));
		$q->close();
		
		$this->loadAllowedValues();
	}
	
	public function id()
	{
		return $this->id;
	}
	
	public function code() 
	{
		return strlen($this->code) ? $this->code : $this->id;
	}
	
	/**
		@param $id  string CODE or numeric ID
		@return numeric ID
	*/
	static public function toID($id)
	{
		if (is_numeric($id)) {
			return (int)$id;
		}

		static $q = null;
		createStaticQuery($q,"
			SELECT id FROM cf_attributes WHERE code=:code
		");
		$q->setParam('code',trim($id));
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("Invalid attribute code: [$id]");
		}
		$id = (int)$q->at(0);
		$q->close();
		
		return $id;
	}
	
	public function name() 
	{
		return $this->name;
	}
	
	public function value()
	{
		return $this->value;
	}
	
	public function isNull()
	{
		return !(bool)$this->value;
	}
	
	public function type()
	{
		return $this->type;
	}
	
	public function length()
	{
		if ($this->type() == AttributeTypes::STRING) {
			return 1024;
		}
		elseif ($this->type() == AttributeTypes::INTEGER) {
			return 10;
		}
		elseif ($this->type() == AttributeTypes::PRICE) {
			return 12;
		}
		elseif ($this->type() == AttributeTypes::FLOAT1) {
			return 11;
		}
		elseif ($this->type() == AttributeTypes::FLOAT2) {
			return 12;
		}
		elseif ($this->type() == AttributeTypes::FLOAT3) {
			return 13;
		}
		
		assert(false);
	}
	
	public function scale()
	{
		if ($this->type() == AttributeTypes::PRICE) {
			return 2;
		}
		elseif ($this->type() == AttributeTypes::FLOAT1) {
			return 1;
		}
		elseif ($this->type() == AttributeTypes::FLOAT2) {
			return 2;
		}
		elseif ($this->type() == AttributeTypes::FLOAT3) {
			return 3;
		}
		return 0;
	}
	
	public function isList()
	{
		return !empty($this->valuesList);
	}

	///array of pairs (0=>id 1=>value)
	public function allowedValues()
	{
		if (!$this->isList()) {
			return false;
		}
		return $this->valuesList;
	}
	
	/** when list - should be ID of value record */
	public function setValue($val)
	{
		if (!is_null($val)) 
		{
			if ($this->isList()) {
				$val = (int)$val;
			}
			else {
				$val = $this->convertValue($val);
			}
		}
		
		if (!$this->isValueAllowed($val)) {
			throw new Exception("
				Invalid value ($val) for attribute [{$this->id()}] {$this->name()}
			");
		}
		
		$this->value = $val;
	}
	
	public function asString()
	{
		if ($this->isList() && !$this->isNull()) 
		{
			foreach ($this->allowedValues() as $pair) 
			{
				if ($pair[0] == $this->value()) {
					return $pair[1];
				}
			}
			throw new Exception("
				Value [{$this->value()}] of attribute [{$this->name()}]
				is not in list of possible values
			");
		}
		return (string)$this->value();
	}
	
	
	public function __toString() 
	{
       return "[{$this->id()}] {$this->name()} = {$this->value()} (type: {$this->type()})";
   }
   
   
   private function loadAllowedValues()
   {   
   	static $q = null;
		createStaticQuery($q,"
			SELECT id, value
			FROM cf_attribute_values
			WHERE attribute_id = :id
			ORDER BY sort_order ASC
		");
		$q->setParam('id',$this->id());
		$q->execute();
		while ($q->fetch()) {
			$this->valuesList[] = array(
				(int)$q->at(0),
				$this->convertValue($q->at(1))
			);
		}
		$q->close();
	}
   
   
   private function isValueAllowed($val)
   {
		if ($this->isList() && $val)
		{
			foreach ($this->valuesList as $allowedVal) {
				if ($val == $allowedVal[0]) {
					return true;
				}
			}
			return false;
		}
		return true;
   }
   
   private function convertValue($val) 
   {
		if ($this->type() == AttributeTypes::INTEGER) {
			return (int)$val;
		}
		return (string)$val;
   }

	
	private $id = null;
	private $code = null;
	private $name = '';
	private $value = null;
	private $type = null;
	private $valuesList = array();
};




/** 
	set of attributes, every unique attribute included only once
*/

class Attributes
	extends ArrayOfItems
{
	public function get($id) 
	{
		$id = Attribute::toID($id);
		foreach ($this->items as $item) 
		{
			if ($item->id() == $id) {
				return $item;
			}
		}
		return false;
	}
	
	public function exist($id) 
	{
		return !($this->get($id) === false);
	}
	
	public function allNull()
	{
		foreach ($this->items as $item) 
		{
			if (!$item->isNull()) {
				return false;
			}
		}
		return true;
	}


	protected function create($id)
	{
		return new Attribute($id);
	}
	
	protected function add(Attribute $item) 
	{
		if (!$this->exist($item->id())) 
		{
			parent::add($item);
			return true;
		}
		return false;
	}
};



class ProductAttribute
	extends Attribute
{
	function __construct($attrId, $productId)
	{
		$attrId = Attribute::toID($attrId);
		$productId = Product::toID($productId);
		
		parent::__construct($attrId);

		static $q = null;
		createStaticQuery($q,"
			SELECT attribute_value
			FROM cf_product_attributes
			WHERE product_id=:productId AND attribute_id=:attrId
		");
		$q->setParam('productId', $productId);
		$q->setParam('attrId', $attrId);
		$q->execute();
		if ($q->fetch()) {
			$this->setValue($q->at(0));
		}
		$q->close();
	}
}



class GroupAttributes
	extends Attributes
{
	function __construct($groupId)
	{
		$groupId = Group::toID($groupId);
		$ancestors = new AncestorGroups($groupId);
		foreach ($ancestors as $ancestor) 
		{
			$attrs = new GroupAttributes($ancestor->id());
			$this->append($attrs);
		}
		
		static $q = null;
		createStaticQuery($q,"
			SELECT attribute_id
			FROM cf_group_attributes
			WHERE group_id=:groupId
			ORDER BY sort_order ASC
		");
		$q->setParam('groupId',$groupId);
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add( $this->create( $q->at(0) ) );
		}
		$q->close();	
	}
};




class ProductGroupAttributes
	extends Attributes
{
	function __construct($productId, $groupId)
	{
		$this->productId = Product::toID($productId);
		$attrs = new GroupAttributes(Group::toID($groupId));
		foreach ($attrs as $attr) {
			$this->add( $this->create($attr->id()) );
		}
	}
	
	protected function create($id)
	{
		return new ProductAttribute($id, $this->productId);
	}
	
	protected $productId = null;
}



class ProductAttributes
	extends Attributes
{
	function __construct($productId, $showDisabled=false)
	{
		$inGroups = new ProductGroups($productId, $showDisabled);
		foreach ($inGroups as $group) 
		{
			$attrs = new ProductGroupAttributes($productId, $group->id());
			foreach ($attrs as $attr) {
				$this->add($attr);
			}
		}
	}
}


class ProductOptionAttribute
	extends Attribute
{
	function __construct($attrId, $optionId)
	{
		$attrId = Attribute::toID($attrId);
		$optionId = ProductOption::toID($optionId);
		
		parent::__construct($attrId);
		
		static $q = null;
		createStaticQuery($q,"
			SELECT attribute_value
			FROM cf_product_options_attributes
			WHERE product_option_id=:optionId AND attribute_id=:attrId
		");
		$q->setParam('optionId',$optionId);
		$q->setParam('attrId',$attrId);
		$q->execute();
		if ($q->fetch()) {
			$this->setValue($q->at(0));
		}
		$q->close();
	}
}

/**
	Takes product option attributes values from DB
*/
class ProductOptionUniqueGroupAttributes
	extends ProductGroupAttributes
{
	function __construct($optionId, $groupId)
	{
		$this->optionId = ProductOption::toID($optionId);
		$option = new ProductOption($optionId);
		parent::__construct($option->productId(), $groupId);
	}
	
	protected function create($id)
	{
		return new ProductOptionAttribute($id, $this->optionId);
	}
	
	
	protected $optionId = null;
}



class ProductOptionUniqueAttributes
	extends Attributes
{
	function __construct($optionId, $showDisabled=false)
	{
		$option = new ProductOption($optionId);
		$inGroups = new ProductGroups($option->productId(), $showDisabled);
		foreach ($inGroups as $group) 
		{
			$attrs = new ProductOptionUniqueGroupAttributes($optionId, $group->id());
			foreach ($attrs as $attr) {
				$this->add($attr);
			}
		}
	}
}



/**
	Takes product attribute value if option attribute value is not set
*/
class ProductOptionGroupAttributes
	extends ProductOptionUniqueGroupAttributes
{
	function __construct($optionId, $groupId)
	{
		parent::__construct($optionId, $groupId);
	}
	
	protected function create($id)
	{
		return ProductGroupAttributes::create($id);		//create product attribute
	}
	
	protected function add(Attribute $productAttr) 
	{
		$optionAttr = new ProductOptionAttribute($productAttr->id(), $this->optionId);
		GroupAttributes::add($optionAttr->isNull() ? $productAttr : $optionAttr);	//override with option attribute it is not null
	}
	

}



/**
	Takes product attribute value if option attribute value is not set
*/

class ProductOptionAttributes
	extends Attributes
{
	function __construct($optionId, $showDisabled=false)
	{
		$option = new ProductOption($optionId);
		$inGroups = new ProductGroups($option->productId(), $showDisabled);
		foreach ($inGroups as $group) 
		{
			$attrs = new ProductOptionGroupAttributes($optionId, $group->id());
			foreach ($attrs as $attr) {
				$this->add($attr);
			}
		}
	}
};


/**
	Each review is array with fields: 'user', 'date', 'review'
*/
class ProductReviews
	extends LimitedArray
{
	function __construct($productId, $skip=null, $limit=null)
	{
		$productId = Product::toID($productId);
		$sql = "
			SELECT cf_users.name AS user_name, dt, review
			FROM cf_product_reviews
			INNER JOIN cf_users ON cf_product_reviews.user_id=cf_users.id
			WHERE product_id=$productId
			ORDER BY dt DESC, cf_product_reviews.id DESC
		";
		parent::__construct($sql,$skip,$limit);
		
		$q = createQuery($this->limitedSql());
		$q->execute();
		while ($q->fetch()) 
		{
			$this->add(
				array(
					'user' => $q->at(0),
					'date' => $q->at(1),
					'review' => $q->at(2)
				)
			);
		}
		$q->close();
	}

	static public function create($productId, $userId, $review)
	{
		return 
			execQuery(
				"INSERT INTO cf_product_reviews (product_id,user_id,review,dt) VALUES(:productId,:userId,:review,NOW())",
				Product::toID($productId),
				$userId,
				$review
			);
	}
}



/**
	Store chosen products in user session
*/

class Basket
{
	function __construct()
	{
		session::open();
		if (session::has('cf_shop_basket')) 
		{
			$this->items = session::get('cf_shop_basket');
		}
	}
	
	/**
		@param $productId  ID or CODE
		@param $optionId   ID or CODE
	*/
	public function add($productId, $optionId, $amount)
	{
		if ($amount == 0) {
			return;
		}
		
		$productId = Product::toID($productId);
		if ($optionId) {
			$optionId = ProductOption::toID($optionId);	
		}
		
		$found = $this->find($productId, $optionId);
		if ($found !== false) {
			++$this->items[$found][2];
		}
		else {
			$this->items[] = array($productId, $optionId, $amount);
		}
		session::set('cf_shop_basket', $this->items);
	}
	
	public function isEmpty()
	{
		return empty($this->items);
	}
	
	public function totalProducts()
	{
		$count = 0;
		foreach ($this->items as $item) 
		{
			$count += $item[2];
		}
		return $count;
	}
	
	public function totalSum()
	{
		$products = $this->toArray();
		$sum = 0;
		foreach ($products as $product) 
		{
			if ($product[1]) {
				$sum += ($product[1]->price() * $product[2]);
			}
			else {
				$sum += ($product[0]->price() * $product[2]);
			}
		}
		return $sum;
	}
	
	public function purge()
	{
		$this->items = array();
		session::set('cf_shop_basket', $this->items);
	}
	
	public function toArray()
	{
		$products = array();
		
		foreach ($this->items as $item)
		{
			$product = new Product($item[0]);
			$manufacturer = new Manufacturer($product->manufacturerId());
			$productOption = $item[1] ? new ProductOption($item[1]) : 0;
			$products[] = array($product, $productOption, $item[2], $manufacturer);
		}
		return $products;
	}
	
	public function __toString()
	{
		$s = '';
		$products = $this->toArray();
		foreach ($products as $product) 
		{
			$name = $product[0]->name().' '.$product[3]->name().' '.$product[0]->model();
			if ($product[1]) {
				$name .= ' - ' . $product[1]->name();
			}
			$price = $product[1] ? $product[1]->price() : $product[0]->price();
			$amount = $product[2];
			$s .= "$name [$price] - $amount\n";
		}
		return $s;
	}
	
	
	
	private function find($productId, $optionId)
	{
		for ($i=0; $i<count($this->items); ++$i)
		{
			$item = $this->items[$i];
			if ($item[0] == $productId && $item[1] == $optionId) 
			{
				return $i;
			}
		}
		return false;
	}
	
	
	private $items = array();
}


class CustomerAddress
{
	function __construct($id)
	{
		$this->id = (int)$id;
		$this->load();
	}
	
	public function name()
	{
		return $this->name;
	}
	
	public function address()
	{
		return $this->address;
	}
	
	public function phone()
	{
		return $this->phone;
	}

	public function comments()
	{
		return $this->comments;
	}
	
	public function update($name,$address,$phone,$comments)
	{
		execQuery(
			"UPDATE cf_customer_addresses 
			SET name=:name,address=:address,phone=:phone,comments=:comments
			WHERE id={$this->id}",
			$name,
			$address,
			$phone,
			$comments
		);
		$this->load();
		return $this;
	}
	
	public function setAsDefault()
	{
		execQuery("UPDATE cf_customers SET default_address_id={$this->id} WHERE id={$this->customerId}");
		return $this;
	}
	
	static public function create($customerId,$name,$address,$phone,$comments)
	{
		execQuery(
			"INSERT INTO cf_customer_addresses (name,address,phone,comments,customer_id)
			VALUES(:name,:address,:phone,:comments,$customerId)",
			$name,
			$address,
			$phone,
			$comments
		);
		
		global $db;
		$id = $db->lastInsertId();
		
		return new CustomerAddress($id);
	}
	
	
	
	private function load()
	{
		static $q = null;
		createStaticQuery($q,"
			SELECT name, address, phone, comments, customer_id
			FROM cf_customer_addresses
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No customer address with id [{$this->id}]");
		}
		
		$this->name = $q->at(0);
		$this->address = $q->at(1);
		$this->phone = $q->at(2);
		$this->comments = $q->at(3);
		$this->customerId = (int)$q->at(4);
		
		$q->close();
	}
	
	private $id = null;
	private $name = null;
	private $address = null;
	private $phone = null;
	private $comments = null;
	private $customerId = null;
}



class Customer
{
	function __construct($userId)
	{
		static $q = null;
		createStaticQuery($q,"
			SELECT id, default_address_id
			FROM cf_customers
			WHERE user_id=:userId
		");
		$q->setParam('userId',$userId);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No customer associated with user id [{$userId}]");
		}
		
		$this->id = $q->at(0);
		$this->defaultAddressId = (int)$q->at(1);
		
		$q->close();		
	}
	
	public function id()
	{
		return $this->id;
	}
	
	public function defaultAddress()
	{
		if ($this->defaultAddressId)
		{
			return new CustomerAddress($this->defaultAddressId);
		}
		return null;
	}
	
	
	static public function create($userId)
	{
		execQuery(
			"INSERT INTO cf_customers (user_id) VALUES(:userId)",
			$userId
		);
		global $db;
		return new Customer($userId);
	}
	
	private $id = null;
	private $defaultAddressId = null;
}





class Order
{
	function __construct($id)
	{
		$this->id = $id;
		
		static $q = null;
		createStaticQuery($q,"
			SELECT state, created
			FROM cf_orders
			WHERE id=:id
		");
		$q->setParam('id',$this->id);
		$q->execute();
		if (!$q->fetch()) {
			throw new Exception("No Order with id [{$this->id}]");
		}
		
		$this->state = $q->at(0);
		$this->createdAt = strtotime($q->at(1));
		
		$q->close();
	}
	
	public function id() 
	{
		return $this->id;
	}
	
	public function state() 
	{
		return $this->state;
	}

	public function createdAt() 
	{
		return $this->createdAt;
	}
	
	public function add($productId, $optionId, $quantity)
	{
		static $q = null;
		createStaticQuery($q,"
			INSERT INTO cf_products_in_orders(
				order_id, product_id, option_id, quantity
			)			
			VALUES(
				:orderId, :productId, :optionId, :quantity
			)
		");
		
		$q->setParam('orderId', $this->id);
		$q->setParam('productId', $productId);
		$q->setParam('optionId', $optionId ? $optionId : null);
		$q->setParam('quantity', $quantity);
		$q->execute();
		$q->close();
	}
	
	public function addBasket($basket)
	{
		$items = $basket->toArray();
		foreach ($items as $item) 
		{
			$this->add(
				$item[0]->id(),
				$item[1] ? $item[1]->id() : 0,
				$item[2]
			);
		}
	}
	
	private $id = null;
	private $state = null;
	private $createdAt = null;
};



class Orders
{
	static public function create(
		$customerName, 
		$customerEmail, 
		$customerPhone, 
		$customerAddress, 
		$customerComments,
		$customerId=null)
	{
		static $q = null;
		createStaticQuery($q,"
			INSERT INTO cf_orders (
				customer_name, customer_email, customer_phone, 
					customer_address, customer_comments, created, state, customer_id)
			VALUES(
				:customerName, :customerEmail, :customerPhone,
					:customerAddress, :customerComments, NOW(), 1, :customerId
			)
		");
		
		$q->setParam('customerName',$customerName);
		$q->setParam('customerEmail',$customerEmail);
		$q->setParam('customerPhone',$customerPhone);
		$q->setParam('customerAddress',$customerAddress);
		$q->setParam('customerComments',$customerComments);
		$q->setParam('customerId',$customerId);
		$q->execute();
		$q->close();
		
		global $db;
		return new Order($db->lastInsertId());
	}
};






abstract class ShopExport
{
	public function export($groupId=null)
	{
		$this->exportGroups($groupId);
		$this->exportProducts($groupId);
	}
	
	abstract protected function exportGroup($group);
	abstract protected function exportProduct($product, $groupId, $manufacturer, $option=null);


	
	private function exportGroups($parentId)
	{
		if (!is_null($parentId))
		{
			$this->exportGroup( new Group($parentId) );
		}
		
		$groups = new ChildGroups($parentId);
		foreach ($groups as $group)
		{	
			$this->exportGroups($group->id());
		}
	}
	
	private function exportProducts($rootId)
	{
		$groups = new DescendantGroups($rootId);
		$ids = array();
		foreach ($groups as $group)
		{
			$ids[] = $group->id();
		}
		if ($rootId)
		{
			$ids[] = $rootId;
		}
		
		$q = createQuery("
			SELECT product_id, group_id
			FROM cf_products_in_groups
			INNER JOIN cf_groups ON cf_products_in_groups.group_id = cf_groups.id
			WHERE group_id IN (" . implode(',',$ids) . ")
			GROUP BY product_id
			ORDER BY cf_groups.sort_order, cf_products_in_groups.sort_order
		");
		$q->execute();
		
		while ($q->fetch())
		{
			$productId = $q->at(0);
			$groupId = $q->at(1);
			$product = new Product($productId);
			$inGroups = new ProductGroups($productId);

			$manufacturer = new Manufacturer($product->manufacturerId());
			
			$this->exportProduct($product, $groupId, $manufacturer);
			
			$options = new ProductOptions($productId);
			foreach ($options as $option)
			{ 
				$this->exportProduct($product, $groupId, $manufacturer, $option);
			}
		}
		
		$q->close();
	}
};

?>